iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0

前言:今天就是之後

昨天結尾說這個時代證明了一件事:一個請求一個 instance,今天親手做。前天第二步還留了一句——宿主認得的 import 是它自己那一套,要自己給,得把它當函式庫嵌進自己的程式,那是之後的事——今天就是之後。三步:先讓 wasmtime 自己當伺服器,看每個請求拿到什麼;再自己寫一個宿主,把同一件事做出來;最後丟一支不會停的 handler 進去,看它炸在哪裡。

第一步:wasmtime 自己就是伺服器

寫一支 handler。它實作的介面叫 wasi:http/incoming-handler,跟昨天那張資料夾清單出自同一套——wasi:http 是 WASI 0.2 七包裡的一包,Rust 這邊有現成的綁定。程式只做一件事:一個全域計數器,每次被叫加一、把數字回出去,主體是這幾行:

// 每個 instance 一個計數器。它會不會往上爬,不是這段程式決定的,是建 instance 的人決定的。
static COUNT: AtomicU32 = AtomicU32::new(0);

struct Handler;

impl Guest for Handler {
    fn handle(_request: IncomingRequest, response_out: ResponseOutparam) {
        let n = COUNT.fetch_add(1, Ordering::Relaxed) + 1;

        let response = OutgoingResponse::new(Fields::new());
        let body = response.body().unwrap();
        ResponseOutparam::set(response_out, Ok(response));

        let stream = body.write().unwrap();
        stream.blocking_write_and_flush(format!("{n}\n").as_bytes()).unwrap();
        drop(stream);
        OutgoingBody::finish(body, None).unwrap();
    }
}

wasip2::http::proxy::export!(Handler);

編法跟昨天差一步。直接編第二代也會出元件,但它會連帶要環境變數、結束碼那些,wasmtime 的伺服器只給 HTTP 那一套,拒收;所以先照第一代編成核心模組,再用 wasmtime 官方的轉接層包成元件。包好的 import 表只剩六項,沒有檔案系統——前天那顆有十六項。然後 wasmtime 有個子命令就是拿來跑這種元件的:

$ wasmtime serve --addr 127.0.0.1:18080 counter-http.wasm
Serving HTTP on http://127.0.0.1:18080/

$ curl http://127.0.0.1:18080/
1
$ curl http://127.0.0.1:18080/
1
$ curl http://127.0.0.1:18080/
1

三次都是 1。前天用 wasmtime 直接叫計數器,連叫兩次都是 1,因為每叫一次都從檔案重新建一個 instance;它的伺服器把同一件事搬到每一個 HTTP 請求上——請求進來才建 instance,回完就丟,下一個請求跟這一個沒有任何關係,所以計數器永遠停在 1。這不是新主意:2019 年 3 月 Fastly 開源 Lucet 的公告寫的設計要求就是「Fastly 處理的每一個請求都要跑一次」,同一個行程裡每秒數萬個請求各一個 instance,一個不到 50 微秒。

Lucet 那篇:https://www.fastly.com/blog/announcing-lucet-fastly-native-webassembly-compiler-runtime

第二步:自己寫那個宿主

宿主是一支自己寫的程式,axum 收 HTTP、wasmtime 跑 handler——wasmtime 第一次不是命令列,是嵌在自己程式裡的函式庫。啟動時把檔案編譯、連結一次,之後每個請求做的事,節錄:

    // 這一個請求自己的 Store。前面任何一個請求留下的東西都不在裡面。
    let mut store = Store::new(
        &app.engine,
        HostState {
            wasi: WasiCtxBuilder::new().build(),
            http: WasiHttpCtx::new(),
            table: ResourceTable::new(),
        },
    );

    // ……(把 HTTP 請求轉交給它的幾行,略)

    let task = tokio::spawn(async move {
        let t = Instant::now();
        let proxy = pre.instantiate_async(&mut store).await?;
        println!("{name}: instantiate {} µs", t.elapsed().as_micros());
        proxy.wasi_http_incoming_handler().call_handle(&mut store, incoming, out).await?;
        anyhow::Ok(())
    });

建 WASI 那一行是前天那件事的程式碼版:什麼都沒給,沒有資料夾、沒有環境變數,它只拿得到 HTTP 那一包。跑起來,同一顆檔一個 byte 沒動:

$ host/target/release/host counter-http.wasm
counter-http: loaded counter-http.wasm
listening on 127.0.0.1:18090
counter-http: instantiate 928 µs
counter-http: instantiate 56 µs
counter-http: instantiate 54 µs

$ curl http://127.0.0.1:18090/counter-http/
1
$ curl http://127.0.0.1:18090/counter-http/
1
$ curl http://127.0.0.1:18090/counter-http/
1

還是 1、1、1,載它的人從 wasmtime 換成自己寫的,檔案沒換,前天那句話第三次。多出來的那欄數字量的是建 instance 那一行:從連結好的樣板建一個新 instance、放進剛開的 Store,不含編譯(啟動時做過一次)、不含網路。第一次接近一毫秒,之後五、六十微秒。這個量級就是「每個請求開一個新的」付得起的原因:幾十微秒開一個、回完就丟,沒有暖機池要養,也沒有上一個請求留下的東西要清。

第三步:炸在自己的 Store 裡

炸彈是同一個形狀的 handler,只是處理請求的那支函式裡一個無窮迴圈,永遠不回。宿主加一個燃料旗標:有給,每個 Store 分一份燃料,燒完就停。加的是這幾行:

    if let Some(fuel) = app.fuel {
        store.set_fuel(fuel)?;
        // 每燒一億燃料就把執行緒交還給 tokio,忙的 guest 不能把別的請求卡住。
        store.fuel_async_yield_interval(Some(100_000_000))?;
    }

兩顆檔一起載,三個炸彈丟背景、立刻打計數器:

$ host/target/release/host --fuel 30000000000 counter-http.wasm loop-bomb.wasm
counter-http: loaded counter-http.wasm
loop-bomb: loaded loop-bomb.wasm
listening on 127.0.0.1:18090
counter-http: instantiate 649 µs
loop-bomb: instantiate 77 µs
loop-bomb: instantiate 78 µs
loop-bomb: instantiate 81 µs
counter-http: instantiate 89 µs
loop-bomb: trap: wasm trap: all fuel consumed by WebAssembly
loop-bomb: trap: wasm trap: all fuel consumed by WebAssembly
loop-bomb: trap: wasm trap: all fuel consumed by WebAssembly

$ curl http://127.0.0.1:18090/counter-http/
1
$ for i in 1 2 3; do curl -s http://127.0.0.1:18090/loop-bomb/ & done; curl -s http://127.0.0.1:18090/counter-http/; wait
1
trap: wasm trap: all fuel consumed by WebAssembly
trap: wasm trap: all fuel consumed by WebAssembly
trap: wasm trap: all fuel consumed by WebAssembly

三百億燃料在這台機器上燒兩秒出頭。三個炸彈還在燒,計數器馬上就回了 1;兩秒多之後三行 trap 才到。燃料是發給那一個 Store 的,燒完炸的也是那一個 Store:宿主還在、旁邊的請求沒感覺、三個炸彈彼此也互不相干——前天說「擋你的是 runtime」,這是 runtime 擋的另一種東西,不是它要不到什麼,是它用太多。燃料之外還有時間跟記憶體上限兩道,形狀一樣。那行 yield 是宿主自己的事:一個不肯讓出執行緒的 guest 會把別的請求卡在門口,所以每燒一億就交還一次,handler 不知道這件事。到這裡,「請求」換成別的東西——一行字、一次呼叫——換掉的只有門口那幾行,每次一個 Store、燃料跟著 Store 走的做法不用改。

結語

一句帶走:一個請求一個 instance:幾十微秒開一個、回完就丟,燃料是那一個 Store 的,炸就炸在裡面;這件事不用等平台,宿主自己寫也做得到。


上一篇
Day 09|同一支程式,兩代 WASI
系列文
Tool Use Is All You Need:30 天用 WebAssembly 試圖控制 Agent 的手腳 共 10 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言